跳到主要内容

SpringSecurity 编写一个简单鉴权Demo

注意,这篇笔记是接着上一篇 SpringSecurity 编写一个简单认证Demo 笔记的项目接着拓展的

Authorization 的一些概念

Principal

身份(Principal),即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principal,但只有一个 Primary principal,一般是用户名/密码/手机号。

Principle 与 User 的差异是,Principal 是指一个可辨识的唯一身份,用来代表一个人,一个公司,一个装置或另一个系统,并不限定于代表一个人,而用户(User)是指与系统互动的操作者(人)。

Granted Authority

在 Spring Security 中,可以将每个 GrantedAuthority 视为一个单独的特权。比如 READ_AUTHORITY,WRITE_PRIVILEGE 甚至 CAN_EXECUTE_AS_ROOT。

当直接使用 GrantedAuthority 时,例如通过使用诸如 hasAuthority('READ_AUTHORITY') 之类的表达式,将以细粒度的方式限制访问。

@Override
protected void configure(HttpSecurity http) throws Exception {
.antMatchers("/protectedbyauthority").hasAuthority("READ_PRIVILEGE")
}

Roles

在 Spring Security 中,可以将每个 Role 视为一个粗粒度的 GrantedAuthority ,此种 GrantedAuthority 以 ROLE 为前缀的字符串表示。当直接使用 Role 时,可使用 hasRole("ADMIN") 之类的表达式,将以粗粒度方式限制访问。值得注意的是,默认的 “ROLE” 前缀是可以配置的。

@Override
protected void configure(HttpSecurity http) throws Exception {
.antMatchers("/protectedbyrole").hasRole("USER")
}

权限的粒度

注意这个 GrantedAuthority 和 Roles 它们的粒度不同,Roles 一般代表更粗粒度的权限

Authority、Role 之间区别

参考资料 spring security中Authority、Role的区别

在编写权限控制时发现有两个种 API,一个是基于 Authority 判断,一个是基于 Role 判断

看注释就能理解,如果你使用的是hasRole方法来判断你的登录用户是否有权限访问某个接口,那么你初始化User时,放入的 GrantedAuthority 的字符就需要包含 ROLE_ 前缀,参见下图红箭头:

而接口的访问却不用加这个 ROLE_ 前缀

反之,如果使用的是 hasAuthority 方法则无需在 GrantedAuthority 的字符加上 ROLE_ 前缀

为什么分 Authority、Role

参考资料 Granted Authority Versus Role in Spring Security

一般 Role 是粗粒度的 Authority,如下所示,使用 Authority 可以标识它是个只读权限

@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
.antMatchers("/protectedbyrole").hasRole("USER")
.antMatchers("/protectedbyauthority").hasAuthority("READ_PRIVILEGE")
// ...
}

而且,对于自定义的数据存储时(SQL),可以让一个 Role 内部包含多个 GrantedAuthority,如下所示

// 这个 Role 类是自定义的,它里面存了一些 GrantedAuthority
private Collection<? extends GrantedAuthority> getAuthorities(Collection<Role> roles) {

List<GrantedAuthority> authorities = new ArrayList<>();

for (Role role: roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));

role.getPrivileges().stream()
.map(p -> new SimpleGrantedAuthority(p.getName()))
.forEach(authorities::add);
}

return authorities;
}

修改下 Controller

对上个示例的 Controller 进行修改,将其分为

  • ResourceController
  • AuthController

@Slf4j
@RestController
public class ResourceController {

@GetMapping("/hello")
public String hello() {
return "<h1>Welcome</h1>";
}

@GetMapping("/user")
public String user() {
return "<h1>Welcome User</h1>";
}

@GetMapping("/admin")
public String admin() {
return "<h1>Welcome Admin</h1>";
}

}

修改下 MyDetailsService

之前这个 MyDetailsService 里面就存一个用户,现在需要添加多个角色,所以相应的用户也应该存多个,因为是测试,所以这里使用 Map 来存这些测试用户

@Service
public class MyDetailsService implements UserDetailsService {

static Map<String, User> users;

static {
users = new HashMap<>();
// 注意:使用 SimpleGrantedAuthority 需要加上 ROLE_ 否则无法比对
users.put("foo",new User("foo", "foopassword",
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))));

users.put("admin", new User("admin","admin", Stream.of(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("ROLE_ADMIN")
).collect(Collectors.toList())));

}

/**
* 这个 UserDetailsService 一般只用于到 DAO 层加载用户数据
*/
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
return users.get(name);
}
}

配置 HttpSecurity

对之前的那个 config 进行一下修改

对上面的各个 API 添加配置一下规则

@Override
public void configure(HttpSecurity http) throws Exception {
// 先关闭 CSRF 防护(跨站请求伪造,其实就是使用 Cookie 的那堆屁事,如果使用 JWT 可以直接关闭它)
http.csrf().disable()
.authorizeRequests()
// 这个 antMatcher 方法用于匹配请求(注意方法名后面要加 's')
.antMatchers(HttpMethod.POST, "/authenticate").permitAll()
.antMatchers("/hello").permitAll() // 任意都可以访问
.antMatchers("/user").hasAnyRole("USER","ADMIN")
.antMatchers("/admin").hasAnyRole("ADMIN")
.anyRequest().authenticated()
// 这里关闭 Session 验证(就是 Cookie-Session 那个)
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);

// 把自己注册的过滤器放在 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}

上面的那种方式也可以使用注解的方式

首先在启动类上加 @EnableGlobalMethodSecurity 注解开启 Security 注解支持

因为默认 @EnableGlobalMethodSecurity 的注解都是单独设置的且全部为 false,所以需要手动开启

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityApplication {

public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}

}

然后就可以直接使用注解了(详情看访问控制那篇笔记)

@Slf4j
@RestController
public class ResourceController {

@GetMapping("/hello")
public String hello() {
return "<h1>Welcome</h1>";
}

@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public String user() {
return "<h1>Welcome User</h1>";
}

@GetMapping("/admin")
@PreAuthorize("hasAnyRole('ADMIN')")
public String admin() {
return "<h1>Welcome Admin</h1>";
}

}

使用 Mock 测试

这里基本和之前那个没区别,只是请求地址换个试试

@Test
void hello() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders.get("/user")
.header("Authorization", "Bearer " + jwt) // 别忘了要加个空格
)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
;
}

接下来可以使用不同的用户登陆访问各个授权 API 试试

Reference

参考资料 How to configure Spring Security Authorization - Java Brains 参考资料 Five Spring Security Concepts - Authentication vs authorization - Java Brains Brain Bytes 参考资料 Spring Security 什么是Principal 参考资料 Spring Security 入门之基本概念